Mac-memory save.md
-------------------

Here is the ultimate, fully merged, and polished guide. It combines the architectural "why" with the bulletproof "how," specifically tailored to protect your Mac's hardware while writing robust code. 

***

# 🧠 Prevent & Detect Memory Leaks on macOS  
**Protect your Mac’s SSD from swap wear and keep your apps snappy.**

## ⚠️ Why leaks are especially dangerous on a Mac (Apple Silicon M1–M4)
Modern Macs use **Unified Memory Architecture** – the same pool serves both RAM and SSD swap. When memory leaks fill the RAM, macOS starts swapping aggressively.  
➜ **Excessive swapping wears out the SSD** (reducing its lifespan) and causes system‑wide lag.  
*Preventing leaks isn’t just about performance – it’s hardware longevity.*

---

## 1. Node.js / Bun (JavaScript & TypeScript)

### 🔹 Replace unbounded caches with an LRU cache
Never use global arrays or standard `Map` objects for caching, as they prevent garbage collection. Use `lru-cache` to enforce strict memory bounds.

```bash
npm install lru-cache
```
```javascript
const { LRUCache } = require('lru-cache');

const options = {
  max: 500,                  // maximum number of items
  maxSize: 50 * 1024 * 1024, // 50 MB hard limit
  sizeCalculation: (value) => JSON.stringify(value).length, // approximate size
  ttl: 1000 * 60 * 10,       // 10-minute time-to-live
};

const cache = new LRUCache(options);
cache.set('key', data);
const value = cache.get('key');
// Items are automatically evicted – zero risk of unbounded memory growth
```

### 🔹 Watch `rss` vs `heapUsed` to detect native leaks
```javascript
setInterval(() => {
  const mem = process.memoryUsage();
  const heap = (mem.heapUsed / 1024 / 1024).toFixed(1);
  const rss = (mem.rss / 1024 / 1024).toFixed(1);
  console.log(`Heap: ${heap} MB | RSS: ${rss} MB`);
}, 5000);
```
- **The Trap:** If `rss` (total RAM) keeps growing while `heapUsed` (JS objects) stays flat, you have a memory leak in a **C++ addon or native binding** (e.g., SQLite, Canvas, Sharp). JS profilers won't catch this!

---

## 2. Playwright / Puppeteer (Browser Automation)

### 🔹 Bulletproof browser closure (Zombie-proof)
*Risk Mitigated:* Node's `process.on('exit')` is strictly synchronous and will **not** wait for `await browser.close()`. This pattern uses OS signals and a `finally` block to guarantee the browser and its macOS "Helper" processes are always killed.

```javascript
const { chromium } = require('playwright');

async function run() {
  const browser = await chromium.launch({
    args: [
      '--js-flags=--expose-gc',   // allows manual GC if needed
      '--disable-dev-shm-usage'   // avoids shared memory issues
    ]
  });

  let isCleaningUp = false;
  const cleanup = async (signal) => {
    if (isCleaningUp) return; // Prevent duplicate cleanup race conditions
    isCleaningUp = true;
    console.log(`\nReceived ${signal}. Closing browser...`);
    try {
      await browser.close();
    } catch (e) {
      console.error("Failed to close browser cleanly:", e);
    }
    // Only force exit if we were interrupted by a signal/crash
    if (signal !== 'normal exit') process.exit(0);
  };

  // Handle Ctrl+C (SIGINT), termination (SIGTERM), and crashes
  process.on('SIGINT', () => cleanup('SIGINT'));
  process.on('SIGTERM', () => cleanup('SIGTERM'));
  process.on('uncaughtException', (err) => {
    console.error('Uncaught Exception:', err);
    cleanup('uncaughtException');
  });

  try {
    const context = await browser.newContext();
    const page = await context.newPage();
    // … your automation …
    await context.close();
  } finally {
    // Handles the "normal, successful end-of-script" scenario
    await cleanup('normal exit');
  }
}

run();
```
*(Note: If using an external supervisor like PM2, ensure `kill_tree: true` is set in your ecosystem file to wipe out orphaned Chromium helper processes).*

---

## 3. Python / Shell Subprocess (Deadlock‑Free)

**🚨 The deadlock trap:** The standard `proc.wait()` with pipes blocks if the child produces output larger than the macOS 64KB pipe buffer. The parent waits forever, the child waits for the pipe to be read → **deadlock**.

### ✅ Safe pattern using `communicate()`
```python
import subprocess
import signal

def run_safe(cmd, timeout=30):
    proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
    try:
        stdout, stderr = proc.communicate(timeout=timeout)
        return stdout, stderr
    except subprocess.TimeoutExpired:
        # Send SIGTERM first for graceful cleanup (allows child to close files/sockets)
        proc.send_signal(signal.SIGTERM)
        try:
            stdout, stderr = proc.communicate(timeout=5)
        except subprocess.TimeoutExpired:
            proc.kill()  # Force kill only if SIGTERM didn't work
            stdout, stderr = proc.communicate() # Drain pipes to prevent zombies
        print(f"Command timed out: {cmd}")
        return None
```
*(💡 **Pro-Tip for Large Files:** Never use `json.load()` on massive files, as it causes 7x RAM bloat. Use the `ijson` library for streaming JSON, or `mmap` for memory-mapped file access).*

---

## 4. macOS Native Leak Detection Tools (Built-in, Zero Install)

Skip heavy third-party tools. Use these built-in terminal commands to instantly profile *any* running process (Node, Python, Rust, C++).

| Goal | Terminal Command | What to look for |
| :--- | :--- | :--- |
| **Check System Swap Wear** | `memory_pressure` | Look at **System-wide memory free %**. If low + high "compressor" activity, your Mac is aggressively swapping to the SSD. |
| **Find the Memory Hogs** | `heap -sortBySize $(pgrep node) \| head -10` | Shows exact objects consuming RAM. Run twice, 5 mins apart. If counts grow, you have a leak. |
| **Check Fragmentation** | `vmmap --summary $(pgrep node)` | Look at **Dirty Size** and **Compressed**. If these never shrink, memory is trapped. |
| **Find Exact C/C++ Leaks** | `MallocStackLogging=1 leaks --atExit -- ./my_app` | The holy grail. Outputs the leaked bytes and the exact **allocation call stack**. |
| **Hunt Zombie Processes** | `ps aux \| awk '{ if ($8 == "Z") print $2, $11 }'` | Finds defunct processes holding PID slots. Kill their parent PID to clear them. |

*(Optional Deep Dive: Open Xcode → Developer Tools → **Instruments** → **Allocations/Leaks** template to visually record and graph memory over time).*

---

## 5. Quick Node.js Heap Watch (For immediate feedback)
Add this to your app to log memory after forcing Garbage Collection. This proves whether memory is actually leaking or just waiting for the GC cycle.

```javascript
if (global.gc) {
  setInterval(() => {
    global.gc();
    console.log('Memory after forced GC:', process.memoryUsage());
  }, 30000);
}
```
**Launch with:**
```bash
node --expose-gc --trace-gc --max-old-space-size=512 myapp.js
```
*(The `--max-old-space-size` flag forces an immediate Out-Of-Memory crash if the heap leaks beyond 512 MB, making hidden leaks fail loudly in CI/CD rather than silently in production).*

---

## ✅ The 10-Second Pre-Commit Checklist
Before merging or deploying your code, verify:
- [ ] Are caches bounded (`lru-cache`) instead of unbounded (`Map`/`Array`)?
- [ ] Are all `setInterval` and `emitter.on` calls paired with a cleanup/teardown method?
- [ ] Are large files streamed (`fs.createReadStream`, `ijson`) instead of loaded entirely into RAM?
- [ ] Do Python subprocesses use `.communicate()` with a timeout to prevent pipe deadlocks?
- [ ] Is Playwright wrapped in signal-handlers and a `finally` block to prevent zombie browsers?


